package com.alkalinesolutions.json.core.impl;

import java.lang.reflect.UndeclaredThrowableException;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Stack;

import com.alkalinesolutions.json.core.CyclicalStructureException;
import com.alkalinesolutions.json.core.UnsupportedTypeException;
import com.alkalinesolutions.json.core.impl.JSONEvent.EventType;

/**
 * Walk a tree of java objects to emit events into a JSON handler.
 * 
 * <p>Java object supported:
 * <table>
 *   <thead>
 *     <tr style='text-decoration:underline'>
 *       <td>Java Type</td>
 *       <td>JSON type</td>
 *     </tr>
 *   </thead>
 *   <tbody>
 *    <tr>
 *      <td>{@link java.lang.Object}[]</td>
 *      <td>Array type<td>
 *    </tr>
 *    <tr>
 *      <td>{@link java.util.List}&lt;{@link java.lang.Object}&gt;</td>
 *      <td>Array type</td>
 *    </tr>
 *    <tr>
 *      <td>{@link java.util.Map}&lt;{@link java.lang.String}, {@link java.lang.Object}&gt;</td>
 *      <td>Object type</td>
 *    </tr>
 *    <tr>
 *      <td>{@link java.lang.Boolean}</td>
 *      <td>Boolean type</td>
 *    </tr>
 *    <tr>
 *      <td><code>null</code></td>
 *      <td>Null type</td>
 *    </tr>
 *    <tr>
 *      <td>{@link java.lang.String}</td>
 *      <td>String type</td>
 *    </tr>
 *    <tr>
 *      <td>{@link java.lang.Number} (e.g. {@link java.lang.Short})</td>
 *      <td>Number type</td>
 *    </tr>
 *  </tbody>
 * </table>
 * 
 * @author David Waite &lt;<a href='mailto:david@alkaline-solutions.com'>david@alkaline-solutions.com</a>&gt;
 */
public class StructureWalker /*implements Walker<List<?>>, Walker<Map<String,?>>, Walker<Object[]> */   {
	
	public static Iterable<JSONEvent> walk(final Object source, final List<? extends Object> array) {
		InternalWalker result = new InternalWalker(source, array, array.iterator());
		return result;
	}
	public  static Iterable<JSONEvent> walk(final Object source, final Object[] array) {
		InternalWalker result = new InternalWalker(source, array, Arrays.asList(array).iterator());
		return result;
	}
	public static Iterable<JSONEvent> walk(final Object source, final Map<String, ? extends Object> object) {
		InternalWalker result = new InternalWalker(source, object, object.entrySet().iterator());
		return result;
	}

	/** Represents a data structure (array, collection, map) and an index
	 *  into said structure as an iterator */
	private static class DataEntry {
		public Object      data;
		public Iterator<?> iterator;
		
		public DataEntry(Object data, Iterator<?> iterator) {
			this.data = data;
			this.iterator = iterator;
		}
		
		public EventType getStartEventType() {
			return data instanceof Map<?,?> 
				? EventType.StartObject : EventType.StartArray;
		}
		public EventType getEndEventType() {
			return data instanceof Map<?,?> 
			? EventType.EndObject : EventType.EndArray;
		}
	}

	protected static class InternalWalker implements Iterator<JSONEvent>, Iterable<JSONEvent> {

		/** dataStack alternates between a struct type and an iterator. The topmost iterator is instead stored in 'iterator' for efficiency */
		private final Object source;
		private final Stack< DataEntry > dataStack;
		private boolean first;
		
		InternalWalker(Object source, Object data, Iterator<?> iterator) {
			if (source == null)
				throw new IllegalArgumentException("source"); //$NON-NLS-1$

			this.source = source;
			dataStack = new Stack< DataEntry >();
			dataStack.push(new DataEntry(data, iterator));
			first = true;
		}
		private void scanStackForSameObject(Object data) throws CyclicalStructureException {
			for(DataEntry dw : dataStack) {
				if (data == dw.data)
					throw new CyclicalStructureException(data);
			}
		}
		public boolean hasNext() {
			return ( first || !dataStack.isEmpty());
		}
	
		public JSONEvent next() {
			try {
				/* if this is the first time iterating into the data entry at
				 * the top of the stack, return a start event.
				 */
				if (first) {
					first = false;
	
					return new JSONEvent(source, 
							dataStack.peek().getStartEventType(), null, null);
				}

				if (!hasNext())
					throw new NoSuchElementException();
		
				/* clear off data entries one by one (each iteration) if they
				 * have no more entries in them.
				 */
				DataEntry dw = dataStack.peek();
				if (!dw.iterator.hasNext()) {
					dataStack.pop();
					return new JSONEvent(source, dw.getEndEventType(), null, null);
				}
		
				/*
				 * Grab the next entry and determine key and data
				 */
				Object data = dw.iterator.next();
				String key = null;
				if (data instanceof Map.Entry<?, ?>) {
					@SuppressWarnings("unchecked")
					Map.Entry<String, Object> entry = (Map.Entry<String, Object>) data;
					key = entry.getKey();
					data = entry.getValue();
				}
			
				switch (JSONType.valueFromObject(data)) {
				case Array:
					scanStackForSameObject(data);
					dataStack.push(new DataEntry(data, 
							Arrays.asList((Object[])data).iterator()));
					return new JSONEvent(source, EventType.StartArray, null, key);
				case List:
					scanStackForSameObject(data);
					dataStack.push(new DataEntry(data, ((List<?>)data).iterator()));
					return new JSONEvent(source, EventType.StartArray, null, key);
				case Object:
					scanStackForSameObject(data);
					dataStack.push(new DataEntry(data, ((Map<?,?>)data).entrySet().iterator()));
					return new JSONEvent(source, EventType.StartObject, null, key);

				case Boolean:
					return new JSONEvent(source, EventType.Boolean, (Boolean) data, key);
				case Number:
					return new JSONEvent(source, EventType.Number, (Number) data, key);
				case String:
					return new JSONEvent(source, EventType.String, (String) data, key);
				case Null:
					return new JSONEvent(source, EventType.Null, null, key);
				default:
					throw new IllegalStateException();
				}
			} catch (UnsupportedTypeException e) {
				throw new UndeclaredThrowableException(e);
			}
		}
	
		public void remove() {
			throw new UnsupportedOperationException();
		}
		
		public Iterator<JSONEvent> iterator() {
			return this;
		}
	}
}
